//
//  
//	GSM-Link - a free SMS messaging library for .NET
//	Copyright (C) 2003 Cris Almodovar
//
//	This library is free software; you can redistribute it and/or
//	modify it under the terms of the GNU Lesser General Public
//	License as published by the Free Software Foundation; either
//	version 2.1 of the License, or (at your option) any later version.
//
//	This library is distributed in the hope that it will be useful,
//	but WITHOUT ANY WARRANTY; without even the implied warranty of
//	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
//	Lesser General Public License for more details.
//
//	You should have received a copy of the GNU Lesser General Public
//	License along with this library; if not, write to the Free Software
//	Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
//
//

using System;
using System.Text;
using System.Text.RegularExpressions;
using System.Collections;
using System.Collections.Specialized;
using System.Runtime.InteropServices;

namespace GsmLink.Comm
{
	public class SerialCommDriver : CommDriver
	{
		#region Fields

		private SerialPort serialPort = new SerialPort();

		#endregion Fields

		#region Constructor, destructor

		public SerialCommDriver()
		{            					
		}

		public SerialCommDriver(string configFile, string profileName) 			
		{			
			Configure(configFile, profileName);			
		}

		~SerialCommDriver()
		{
			try
			{
				if (isStarted)
					Stop();				
			}
			catch {}
		}

		#endregion Constructor, desctructor

		#region Properties	

//		public SerialPort SerialPort
//		{
//			get { return this.serialPort; }
//		}	
		
		// NOTE: If the communication driver
		// has its own config file then you
		// must override these two properties
		// of the CommDriver base class.

//		public override bool HasOwnConfigFile
//		{
//			get { return true; }
//		}
//		
//		public override string ConfigFile
//		{
//			get { return @".\SerialCommDriver.config"; }
//		}
		
		#endregion Properties

		#region Methods

		public override void Configure(string configFile, string profileName)
		{		
			if (!isStarted)
			{
				isConfigured = false;

				// Call the base class' Configure method
				// to load the contents of the config file
				// into the Settings collection.
				base.Configure(configFile, profileName);						

				if (Settings != null && Settings.Count > 0)
				{
					ConfigureSerialPort();					
				}
				else
				{
					if (profileName.ToUpper() == "DEFAULT")
					{
						this.serialPort.LoadDefaults();						
					}
					else if (profileName.ToUpper() == "COM1")
					{
						this.serialPort.LoadDefaults();	
						this.serialPort.PortName = "COM1";
					}
					else if (profileName.ToUpper() == "COM2")
					{
						this.serialPort.LoadDefaults();	
						this.serialPort.PortName = "COM2";
					}
					else if (profileName.ToUpper() == "COM3")
					{
						this.serialPort.LoadDefaults();	
						this.serialPort.PortName = "COM3";
					}
					else if (profileName.ToUpper() == "COM4")
					{
						this.serialPort.LoadDefaults();	
						this.serialPort.PortName = "COM4";
					}
                    else if (profileName.ToUpper() == "COM5")
                    {
                        this.serialPort.LoadDefaults();
                        this.serialPort.PortName = "COM5";
                    }
                    else if (profileName.ToUpper() == "COM6")
                    {
                        this.serialPort.LoadDefaults();
                        this.serialPort.PortName = "COM6";
                    }
                    else if (profileName.ToUpper() == "COM7")
                    {
                        this.serialPort.LoadDefaults();
                        this.serialPort.PortName = "COM7";
                    }
                    else if (profileName.ToUpper() == "COM8")
                    {
                        this.serialPort.LoadDefaults();
                        this.serialPort.PortName = "COM8";
                    }
                    else if (profileName.ToUpper() == "COM9")
                    {
                        this.serialPort.LoadDefaults();
                        this.serialPort.PortName = "COM9";
                    }
                    else
					{					
						// Either there is no config file, there is no entry for "profileName" in
						// the config file, or the config file is invalid/corrupted. The 
						// default values of the SerialPort class' properties will be used.

						isConfigured = false;
						string errorMsg = "Cannot load configuration for the SerialCommDriver " +
							"instance because configuration parameters are invalid.";

						throw new ApplicationException(errorMsg);
					}					
				}

				isConfigured = true;			
			}
			else
			{
				string errorMsg = "Cannot invoke the Configure method on the SerialCommDriver " +
					"instance because it is already started.";

				throw new ApplicationException(errorMsg);
			}
		}

		public override void Start()
		{			
			if (isStarted)
				return;

			if (isConfigured)
			{
				if (!this.serialPort.IsOpen)
					this.serialPort.Open();

				isStarted = true;			
			}
			else
			{
				string errorMsg = "Cannot invoke the Start method on the SerialCommDriver " +
									"instance because it has not been configured yet.";

				throw new ApplicationException(errorMsg);
			}
		}

		public override void Stop()
		{
			if (isStarted)
			{
				// Perform cleanup
				if (this.serialPort.IsOpen)
					this.serialPort.Close();
			
				isStarted = false;				
			}
			else
			{
				string errorMsg = "Cannot invoke the Stop method on the SerialCommDriver " +
					"instance because it has not been started.";

				throw new ApplicationException(errorMsg);
			}		
		}

		private void ConfigureSerialPort()
		{
			// Assumption: The SerialCommDriver is currently stopped.
			if (!isStarted)
			{				
				// Settings["portName"] is the only setting that must be 
				// present in the config file; if the other settings 
				// are not specified then use default values for them.
				if (Settings["portName"] != null)
				{ 
					this.serialPort.PortName = Settings["portName"];
				}
				else
				{
					string errorMsg = "The \"portName\" setting is missing.";
					throw new ApplicationException(errorMsg);
				}

				if (Settings["baudRate"] != null)
					this.serialPort.BaudRate = Convert.ToInt32(Settings["baudRate"]);
				else
					this.serialPort.BaudRate = SerialPort.DEFAULT_BAUD_RATE;

				if (Settings["dataBits"] != null)
					this.serialPort.DataBits = (DataBits)Convert.ToInt32(Settings["dataBits"]);		
				else
					this.serialPort.DataBits = SerialPort.DEFAULT_DATA_BITS;
					
				if (Settings["parity"] != null)
				{
					switch (Settings["parity"].ToUpper())
					{
						case "NONE":
							this.serialPort.Parity = Parity.None;
							break;

						case "ODD":
							this.serialPort.Parity = Parity.Odd;
							break;

						case "EVEN":
							this.serialPort.Parity = Parity.Even;
							break;

						case "MARK":
							this.serialPort.Parity = Parity.Mark;
							break;

						case "SPACE":
							this.serialPort.Parity = Parity.Space;
							break;
				
						default:
							string errorMsg = "'" + Settings["parity"] + "' is not a valid Parity value.";
							throw new ApplicationException(errorMsg);
							//break;
					}
				}
				else
					this.serialPort.Parity = SerialPort.DEFAULT_PARITY;
				
				if (Settings["stopBits"] != null)
				{
					switch (Settings["stopBits"].ToUpper())
					{
						case "1":
							this.serialPort.StopBits = StopBits.One;
							break;

						case "1.5":
							this.serialPort.StopBits = StopBits.OnePointFive;
							break;

						case "2":
							this.serialPort.StopBits = StopBits.Two;
							break;
				
						default:
							string errorMsg = "'" + Settings["stopBits"] + "' is not a valid StopBits value.";
							throw new ApplicationException(errorMsg);
							//break;
					}
				}
				else
					this.serialPort.StopBits = SerialPort.DEFAULT_STOP_BITS;
			
				if (Settings["flowControl"] != null)
				{
					switch (Settings["flowControl"].ToUpper())
					{
						case "NONE":
							this.serialPort.FlowControl = FlowControl.None;
							break;

						case "XONXOFF":
							this.serialPort.FlowControl = FlowControl.XonXoff;
							break;

						case "CTSRTS":
							this.serialPort.FlowControl = FlowControl.CtsRts;
							break;

						case "CTSDTR":
							this.serialPort.FlowControl = FlowControl.CtsDtr;
							break;

						case "DSRRTS":
							this.serialPort.FlowControl = FlowControl.DsrRts;
							break;

						case "DSRDTR":
							this.serialPort.FlowControl = FlowControl.DsrDtr;						
							break;
				
						default:
							string errorMsg = "'" + Settings["flowControl"] + "' is not a valid FlowControl value.";
							throw new ApplicationException(errorMsg);
							//break;
					}
				}
				else
					this.serialPort.FlowControl = SerialPort.DEFAULT_FLOW_CONTROL;
				
				if (Settings["inputBufferSize"] != null)
					this.serialPort.InputBufferSize = Convert.ToInt32(Settings["inputBufferSize"]);
				else
					this.serialPort.InputBufferSize = SerialPort.DEFAULT_BUFFER_SIZE;

				if (Settings["outputBufferSize"] != null)
					this.serialPort.OutputBufferSize = Convert.ToInt32(Settings["outputBufferSize"]);
				else
					this.serialPort.OutputBufferSize = SerialPort.DEFAULT_BUFFER_SIZE;

				if (Settings["readTimeout"] != null)
					this.serialPort.ReadTimeout = Convert.ToInt32(Settings["readTimeout"]);
				else
					this.serialPort.ReadTimeout = SerialPort.DEFAULT_READ_TIMEOUT;
				
				if (Settings["writeTimeout"] != null)
					this.serialPort.WriteTimeout = Convert.ToInt32(Settings["writeTimeout"]);
				else
					this.serialPort.WriteTimeout = SerialPort.DEFAULT_WRITE_TIMEOUT;
				
			}
			else
			{
				string errorMsg = "Cannot configure serial port because the SerialCommDriver " +
					"instance is already started.";

				throw new ApplicationException(errorMsg);
			}
		}

		public override bool SendCommand(string command)
		{
			bool isSuccessful = false;
			if (isStarted)
			{	
				if (command != null && command.Length > 0)
				{
					byte[] bytes = System.Text.Encoding.ASCII.GetBytes(command);				
					lock (this.serialPort)
					{
						this.serialPort.Clear();
						isSuccessful = this.serialPort.Write(bytes);				
					}
				}
			}
			else
			{			
				string errorMsg = "Cannot invoke the SendCommand method on the SerialCommDriver " +
					"instance because it has not been started.";
				throw new ApplicationException(errorMsg);				
			}
			return isSuccessful;
		}
		
		public override string GetResponse(string[] endTokens, TimeSpan timeout)
		{
			StringBuilder buffer = new StringBuilder();
			string response = "";
			
			if (isStarted)
			{				
				// Create the regex pattern for use in 
				// matching (string) tokens that mark  
				// the end of the response stream.
				string termPattern = "";
				if (endTokens != null && endTokens.Length > 0)
				{
					StringBuilder sb = new StringBuilder();
					
					sb.Append("(");
					for(int i = 0; i < endTokens.Length; i++)
					{			
						sb.Append("(");
						sb.Append(endTokens[i]);		
						sb.Append(")");					
						if (i < (endTokens.Length - 1))
							sb.Append("|");
					}
					sb.Append(")");					
					termPattern = sb.ToString();
				}
				else
				{
					termPattern = "."; // Match any character
				}
				
				lock (this.serialPort)
				{
					DateTime start = DateTime.Now;
					TimeSpan duration;

					do
					{
						byte[] bytes = this.serialPort.Read();					
						string tempResponse = System.Text.Encoding.ASCII.GetString(bytes);
						buffer.Append(tempResponse);						
                        duration = DateTime.Now - start;						
					}
					while ((!Regex.IsMatch(buffer.ToString(), termPattern)) && (duration < timeout));				
				}								

				response = buffer.ToString();

			}
			else
			{
				string errorMsg = "Cannot invoke the GetResponse method on the SerialCommDriver " +
					"instance because it has not been started.";
				throw new ApplicationException(errorMsg);				
			}	
		
			return response;
		}

		public override string Transact(string command, string[] endTokens, TimeSpan timeout)
		{
			string response = "";
			
			if (isStarted)
			{
				bool isCommandSent = false;
				lock (this.serialPort)
				{
					isCommandSent = SendCommand(command);
					if (isCommandSent)														
						response = GetResponse(endTokens, timeout);					
				}
				if (!isCommandSent)
				{
					string errorMsg = "An error occurred while sending the command.";
					throw new ApplicationException(errorMsg);				
				}				
			}
			else
			{
				string errorMsg = "Cannot invoke the Transact method on the SerialCommDriver " +
					"instance because it has not been started.";
				
				throw new ApplicationException(errorMsg);				
			}		
	
			return response;
		}

		public override string Transact(string[] commands, string[][] endTokens, string[] successTokens, 
			out string[] responses, TimeSpan pauseTime, TimeSpan timeout)
		{	
			string lastResponse = "";

			if (commands.GetLength(0) > 0 && commands.GetLength(0) == endTokens.GetLength(0) &&  
				commands.GetLength(0) == successTokens.GetLength(0))
			{	
				lock (this.serialPort)
				{
					responses = new string[commands.GetLength(0)];
					for (int i = 0; i < commands.GetLength(0); i++)
					{					
						lastResponse = Transact(commands[i], endTokens[i], timeout);
						if (Regex.IsMatch(lastResponse, successTokens[i]))						
							responses[i] = lastResponse;						
						else						
							break;
						
						if (pauseTime > TimeSpan.Zero)
							System.Threading.Thread.Sleep(pauseTime);
					}
				}
			}	
			else
			{
				string errorMsg = "Cannot invoke Transact on the SerialCommDriver instance " +
					"because some parameters are invalid.";

				throw new ApplicationException(errorMsg);
			}
			
			return lastResponse;
		}

		
		
		#endregion Methods
	}

	
	/// <summary>
	/// Represents a Win32 serial port configured for non-overlapped operation. 
	/// </summary>
	/// <remarks>
	/// This class supports non-overlapped I/O mode only. It can only be used in 
	/// the Windows platform because of its dependency on the Win32 serial 
	/// communication API.
	/// </remarks>
	
	// TODO: 	
	// 1. Add properties: XonLimit, XoffLimit, EofChar, ErrorChar, XonChar, 
	//    XoffChar, EventChar.
	// 2. Add event: DataReceived - passes on to client all data in the 
	//    input buffer.
	// 3. Add property: IsAutoReceive - controls whether or not the DataReceived 
	//    event is raised. 
	// 4. Use a "lock" statement in every ReadFile() or WriteFile() method call. 
	//    Only one thread should be able to read and/or write to the serial port 
	//    at any time.
	//	  
	// NOTE: These properties and events aren't really needed if all we need to do 
	//       is communicate with a GSM device.
	
	internal class SerialPort 
	{
		#region Fields

		public const string DEFAULT_PORT_NAME = "COM1";
		public const int DEFAULT_BAUD_RATE = 19200;
		public const Parity DEFAULT_PARITY = Parity.None;
		public const DataBits DEFAULT_DATA_BITS = DataBits.Eight;
		public const StopBits DEFAULT_STOP_BITS = StopBits.One;
		public const FlowControl DEFAULT_FLOW_CONTROL = FlowControl.None;
		public const int DEFAULT_BUFFER_SIZE = 1024;
		public const int DEFAULT_READ_TIMEOUT = 500;
		public const int DEFAULT_WRITE_TIMEOUT = 500;		

		private bool isOpen = false;		
		private IntPtr handle = SerialCommAPI.INVALID_HANDLE_VALUE;
		private string portName = DEFAULT_PORT_NAME;
		private int baudRate = DEFAULT_BAUD_RATE;
		private Parity parity = DEFAULT_PARITY;
		private DataBits dataBits = DEFAULT_DATA_BITS;
		private StopBits stopBits = DEFAULT_STOP_BITS;
		private FlowControl flowControl = DEFAULT_FLOW_CONTROL;
		private int inputBufferSize = DEFAULT_BUFFER_SIZE;
		private int outputBufferSize = DEFAULT_BUFFER_SIZE;
		private int readTimeout = DEFAULT_READ_TIMEOUT;
		private int writeTimeout = DEFAULT_WRITE_TIMEOUT;

		private SerialCommAPI.DCB dcb;
		private SerialCommAPI.COMMTIMEOUTS timeouts;
	
		#endregion Fields

		#region Constructor, destructor
		
		public SerialPort()
		{
		}

		public SerialPort(string portName)
		{
			// Let the property setter validate the portName value.
			PortName = portName;
		}

		public SerialPort(string portName, int baudRate, DataBits dataBits, Parity parity, 
			StopBits stopBits)
		{
			// Let the property setters validate the values.
			PortName = portName;
			BaudRate = baudRate;
			DataBits = dataBits;
			Parity = parity;
			StopBits = stopBits;			
		}

		public SerialPort(string portName, int baudRate, DataBits dataBits, Parity parity, 
			StopBits stopBits, FlowControl flowControl)
		{
			// Let the property setters validate the values.
			PortName = portName;
			BaudRate = baudRate;
			DataBits = dataBits;
			Parity = parity;
			StopBits = stopBits;
			FlowControl = flowControl;
		}

		public SerialPort(string portName, int baudRate, DataBits dataBits, Parity parity, 
			StopBits stopBits, FlowControl flowControl, int readTimeout, int writeTimeout)
		{
			// Let the property setters validate the values first.
			PortName = portName;
			BaudRate = baudRate;
			DataBits = dataBits;
			Parity = parity;
			StopBits = stopBits;
			FlowControl = flowControl;
			ReadTimeout = readTimeout;
			WriteTimeout = writeTimeout;			
		}
	
		public SerialPort(string portName, int baudRate, DataBits dataBits, Parity parity, 
			StopBits stopBits, FlowControl flowControl, int readTimeout, int writeTimeout,
			int inputBufferSize, int outputBufferSize)
		{
			// Let the property setters validate the values first.
			PortName = portName;
			BaudRate = baudRate;
			DataBits = dataBits;
			Parity = parity;
			StopBits = stopBits;
			FlowControl = flowControl;
			ReadTimeout = readTimeout;
			WriteTimeout = writeTimeout;
			InputBufferSize = inputBufferSize;
			OutputBufferSize = outputBufferSize;			
		}	

		~SerialPort()
		{
			try
			{
				if (!this.isOpen)
					Close();
			}
			catch {}
		}		

		#endregion Constructor, destructor

		#region Properties

		public bool IsOpen
		{
			get	{ return this.isOpen; }
		}

		public string PortName
		{
			get { return this.portName; }
			set 
			{
				if (!this.isOpen)				
				{
					if ((value != null && value != "") && Regex.IsMatch(value.ToUpper(), @"COM\d+"))
					{						
						this.portName = value;
					}
					else
					{
						string errorMsg = "The PortName property cannot be null or blank.";
						throw new ApplicationException(errorMsg);
					}
				}
				else
				{
					string errorMsg = "Cannot change settings while serial port is open.";
					throw new ApplicationException(errorMsg);
				}
			}
		}		

		public int BaudRate
		{
			get { return this.baudRate; }
			set 
			{
				if (!this.isOpen)
				{
					// Limit the baud rate to only these values:
					if (value == 2400 || value == 4800 || value == 9600 || 	value == 19200 ||
						value == 38400 || value == 57600)
					{
						this.baudRate = value;				
					}
					else
					{
						string errorMsg = value + " is not a valid BaudRate value.";
						throw new ApplicationException(errorMsg);
					}

				}
				else
				{
					string errorMsg = "Cannot change settings while serial port is open.";
					throw new ApplicationException(errorMsg);
				}
			}
		}		

		public DataBits DataBits
		{
			get { return this.dataBits; }
			set 
			{
				if (!this.isOpen)				
				{
					if (value == DataBits.Five || value == DataBits.Six || value == DataBits.Seven || 
						value == DataBits.Eight)
					{
						this.dataBits = value;	
					}
					else
					{					
						string errorMsg = value + " is not a valid DataBits value.";
						throw new ApplicationException(errorMsg);					
					}
				}
				else
				{
					string errorMsg = "Cannot change settings while serial port is open.";
					throw new ApplicationException(errorMsg);
				}
			}
		}

		public Parity Parity
		{
			get { return this.parity; }
			set 
			{
				if (!this.isOpen)
				{
					if (value == Parity.None || value == Parity.Odd || value == Parity.Even ||
						value == Parity.Mark || value == Parity.Space)
					{
						this.parity = value;				
					}
					else
					{
						string errorMsg = value + " is not a valid Parity value.";
						throw new ApplicationException(errorMsg);
					}
				}
				else
				{
					string errorMsg = "Cannot change settings while serial port is open.";
					throw new ApplicationException(errorMsg);
				}
			}
		}

		public StopBits StopBits
		{
			get { return this.stopBits; }
			set 
			{
				if (!this.isOpen)
				{
					if (value == StopBits.One || value == StopBits.OnePointFive ||
						value == StopBits.Two)
					{
						this.stopBits = value;				
					}
					else
					{					
						string errorMsg = value + " is not a valid StopBits value.";
						throw new ApplicationException(errorMsg);					
					}

				}
				else
				{
					string errorMsg = "Cannot change settings while serial port is open.";
					throw new ApplicationException(errorMsg);
				}
			}
		}

		public FlowControl FlowControl
		{
			get { return this.flowControl; }
			set 
			{
				if (!this.isOpen)
				{
					if (value == FlowControl.None || value == FlowControl.XonXoff ||
						value == FlowControl.CtsRts || value == FlowControl.DsrDtr)
					{
						this.flowControl = value;				
					}
					else
					{
						string errorMsg = value + " is not a valid FlowControl value.";
						throw new ApplicationException(errorMsg);		
					}
				}
				else
				{
					string errorMsg = "Cannot change settings while serial port is open.";
					throw new ApplicationException(errorMsg);
				}
			}
		}
		
		public int InputBufferSize
		{
			get { return this.inputBufferSize; }
			set 
			{
				if (!this.isOpen)
				{
					if (value > 0)
					{
						this.inputBufferSize = value;				
					}
					else
					{
						string errorMsg = value + " is not a valid InputBufferSize value.";
						throw new ApplicationException(errorMsg);		
					}
				}
				else
				{
					string errorMsg = "Cannot change settings while serial port is open.";
					throw new ApplicationException(errorMsg);
				}
			}
		}

		public int OutputBufferSize
		{
			get { return this.outputBufferSize; }			
			set 
			{
				if (!this.isOpen)
				{
					if (value > 0)
					{
						this.outputBufferSize = value;				
					}
					else
					{
						string errorMsg = value + " is not a valid OutputBufferSize value.";
						throw new ApplicationException(errorMsg);		
					}
				}
				else
				{
					string errorMsg = "Cannot change settings while serial port is open.";
					throw new ApplicationException(errorMsg);
				}
			}
		}

		public int ReadTimeout
		{
			get { return this.readTimeout; }
			set 
			{
				if (!this.isOpen)
				{
					if (value > 0)
					{
						this.readTimeout = value;				
					}
					else
					{
						string errorMsg = value + " is not a valid ReadTimeout value.";
						throw new ApplicationException(errorMsg);		
					}
				}
				else
				{
					string errorMsg = "Cannot change settings while serial port is open.";
					throw new ApplicationException(errorMsg);
				}
			}
		}

		public int WriteTimeout
		{
			get { return this.writeTimeout; }			
			set 
			{
				if (!this.isOpen)
				{
					if (value > 0)
					{
						this.writeTimeout = value;				
					}
					else
					{
						string errorMsg = value + " is not a valid WriteTimeout value.";
						throw new ApplicationException(errorMsg);		
					}
				}
				else
				{
					string errorMsg = "Cannot change settings while serial port is open.";
					throw new ApplicationException(errorMsg);
				}
			}
		}

		
		#endregion Properties

		#region Methods

		public void Open()
		{			
			InternalOpen(); 		
		}

		public void Open(string portName)
		{
			PortName = portName;
			InternalOpen();
		}

		public void Open(string portName, int baudRate, DataBits dataBits, Parity parity, 
			StopBits stopBits)
		{
			// Let the property setters validate the values first.
			PortName = portName;
			BaudRate = baudRate;
			DataBits = dataBits;
			Parity = parity;
			StopBits = stopBits;

			InternalOpen(); 
		}
		
		public void Open(string portName, int baudRate, DataBits dataBits, Parity parity, 
			StopBits stopBits, FlowControl flowControl)
		{
			// Let the property setters validate the values first.
			PortName = portName;
			BaudRate = baudRate;
			DataBits = dataBits;
			Parity = parity;
			StopBits = stopBits;
			FlowControl = flowControl;

			InternalOpen(); 
		}	

		public void Open(string portName, int baudRate, DataBits dataBits, Parity parity, 
			StopBits stopBits, FlowControl flowControl, int readTimeout, int writeTimeout)
		{
			// Let the property setters validate the values first.
			PortName = portName;
			BaudRate = baudRate;
			DataBits = dataBits;
			Parity = parity;
			StopBits = stopBits;
			FlowControl = flowControl;
			ReadTimeout = readTimeout;
			WriteTimeout = writeTimeout;

			InternalOpen(); 
		}
	
		public void Open(string portName, int baudRate, DataBits dataBits, Parity parity, 
			StopBits stopBits, FlowControl flowControl, int readTimeout, int writeTimeout,
			int inputBufferSize, int outputBufferSize)
		{
			// Let the property setters validate the values first.
			PortName = portName;
			BaudRate = baudRate;
			DataBits = dataBits;
			Parity = parity;
			StopBits = stopBits;
			FlowControl = flowControl;
			ReadTimeout = readTimeout;
			WriteTimeout = writeTimeout;
			InputBufferSize = inputBufferSize;
			OutputBufferSize = outputBufferSize;

			InternalOpen(); 
		}	
	
		private void InternalOpen()
		{
			// Check if the serial port is already open. 
			if (this.isOpen)
			{
				string errorMSg = "Cannot open the serial port because it is already open.";
				throw new ApplicationException(errorMSg);
			}

			// Assumption: The current values of all relevant fields/properties 
			// are all valid. 
			
			this.handle = SerialCommAPI.CreateFile(this.portName, SerialCommAPI.GENERIC_READ | 
				SerialCommAPI.GENERIC_WRITE, 0, IntPtr.Zero, SerialCommAPI.OPEN_EXISTING,
				0, IntPtr.Zero);			
			
			if (this.handle == SerialCommAPI.INVALID_HANDLE_VALUE)
			{
				string errorMsg = "Cannot open serial port: " + this.portName;
				throw new ApplicationException(errorMsg);
			}			
			
			// Configure the serial port.				

			bool isSuccessful = InternalClear(true, true, true);
			if (!isSuccessful)
			{
				string errorMsg = "Cannot clear previous serial port errors " +
					"and flush I/O buffers.";
				throw new ApplicationException(errorMsg);
			}                

			// Get the serial port's existing DCB. 
			isSuccessful = SerialCommAPI.GetCommState(this.handle, out this.dcb);
			if (!isSuccessful)
			{
				string errorMsg = "Cannot get Device Control Block for serial port.";
				throw new ApplicationException(errorMsg);
			}

			char parityChar = this.parity.ToString()[0];
			string stopBitsString = "";
			switch (this.stopBits)
			{
				case StopBits.One:
					stopBitsString = "1";
					break;
				case StopBits.OnePointFive:
					stopBitsString = "1.5";
					break;
				case StopBits.Two:
					stopBitsString = "2";
					break;
			}
			string dcbString = String.Format("baud={0} parity={1} data={2} stop={3}",
				this.baudRate, parityChar, (int)this.dataBits, stopBitsString);

			isSuccessful = SerialCommAPI.BuildCommDCB(dcbString, ref this.dcb);				
			if (!isSuccessful)		
			{
				string errorMsg = "Cannot build Device Control Block for serial port.";
				throw new ApplicationException(errorMsg);						
			}						
			
			// NOTE: The FlowControl property is currently not used;
			// the setting for FlowControl is assumed to be always 
			// FlowControl.None.

			// TODO: Add methods: SetupParity() and SetupFlowControl()

			isSuccessful = SerialCommAPI.SetCommState(this.handle, ref this.dcb);
			if (!isSuccessful)
			{
				string errorMsg = "Cannot set Device Control Block for serial port.";
				throw new ApplicationException(errorMsg);
			}

			// Set up timeout values.
			isSuccessful = SerialCommAPI.GetCommTimeouts(this.handle, out this.timeouts);
			if (!isSuccessful)
			{
				string errorMsg = "Cannot get COMMTIMEOUTS for the serial port.";
				throw new ApplicationException(errorMsg);
			}
				
			this.timeouts.ReadIntervalTimeout = 0;						
			this.timeouts.ReadTotalTimeoutMultiplier = 1;
			this.timeouts.ReadTotalTimeoutConstant = (uint)this.readTimeout;
			this.timeouts.WriteTotalTimeoutMultiplier = 1;
			this.timeouts.WriteTotalTimeoutConstant = (uint)this.writeTimeout;

			isSuccessful = SerialCommAPI.SetCommTimeouts(this.handle, ref this.timeouts);
			if (!isSuccessful)
			{
				string errorMsg = "Cannot set timeout values for the serial port.";
				throw new ApplicationException(errorMsg);
			}
            
			// Set up the sizes of the input and output buffers.					
			isSuccessful = SerialCommAPI.SetupComm(this.handle, (uint)this.inputBufferSize, 
				(uint)this.outputBufferSize);									
			if (!isSuccessful)
			{
				string errorMsg = "Cannot set the sizes of the serial port's I/O buffers.";
				throw new ApplicationException(errorMsg);
			}										
			
			// The serial port was successfully opened.							
			this.isOpen = true;			
		}

		public void Close()
		{
			if (this.isOpen)
			{
				this.isOpen = false;
				if (this.handle != SerialCommAPI.INVALID_HANDLE_VALUE)
				{
					bool success = SerialCommAPI.CloseHandle(this.handle);
					if (!success)
					{
						string errorMsg = "Cannot close the serial port.";
						throw new ApplicationException(errorMsg);
					}
				}				
			}
		}
		
		public void LoadDefaults()
		{
			if (!this.isOpen)
			{
				this.handle = (IntPtr)SerialCommAPI.INVALID_HANDLE_VALUE;
				this.portName = DEFAULT_PORT_NAME;
				this.baudRate = DEFAULT_BAUD_RATE;
				this.parity = DEFAULT_PARITY;
				this.dataBits = DEFAULT_DATA_BITS;
				this.stopBits = DEFAULT_STOP_BITS;
				this.flowControl = DEFAULT_FLOW_CONTROL;
				this.inputBufferSize = DEFAULT_BUFFER_SIZE;
				this.outputBufferSize = DEFAULT_BUFFER_SIZE;
				this.readTimeout = DEFAULT_READ_TIMEOUT;
				this.writeTimeout = DEFAULT_WRITE_TIMEOUT;				
			}
			else
			{
				string errorMsg = "Cannot change serial port settings while it is open.";
				throw new ApplicationException(errorMsg);
			}
		}

		// NOTE: The length of the byte array returned is exactly
		// the number of bytes read from the input buffer; if the
		// length is zero, then no data was read from the serial
		// port.
		public byte[] Read(int numberOfBytesToRead)
		{
			if (!this.isOpen)			
			{
				string errorMsg = "Cannot invoke the Read method on the SerialPort " +
					"instance because the serial port is not open.";
				throw new ApplicationException(errorMsg);
			}
			
			if (numberOfBytesToRead > 0)
			{
				byte[] bytes = new byte[numberOfBytesToRead];
				uint numberOfBytesRead;
		
				bool isSuccessful = SerialCommAPI.ReadFile(this.handle, bytes, (uint)numberOfBytesToRead,
					out numberOfBytesRead, IntPtr.Zero);

				if (isSuccessful)
				{
					if (numberOfBytesRead < (uint)numberOfBytesToRead)
					{
						byte[] bytesCopy = new byte[(int)numberOfBytesRead];					
						Array.Copy(bytes, bytesCopy, (int)numberOfBytesRead);					
						bytes = bytesCopy;
					}
				}
				else
				{
					string errorMsg = "Cannot read data from serial port.";
					throw new ApplicationException(errorMsg);
				}
		
				return bytes;			
			}
			else
			{
				return new byte[0];
			}			
		}

		// Reads all data from the input buffer until 
		// no more data arrives - i.e. until ReadTotalTimeout expires.
		public byte[] Read()
		{			
			if (!this.isOpen)
			{
				string errorMsg = "Cannot invoke the Read method on the SerialPort " +
					"instance because the serial port is not open.";
				throw new ApplicationException(errorMsg);
			}				

			byte[] bytes = null;
			ArrayList buffer = new ArrayList();

			while (true)
			{
				bytes = Read(1);
				if (bytes.GetLength(0) == 1)
				{					
					buffer.Add(bytes[0]);                
				}
				else
				{
					break;
				}
			}
			if (buffer.Count > 0)
			{
				bytes = new byte[buffer.Count];
				buffer.CopyTo(bytes);				
			}

			return bytes; 			
		}

		/// <summary>
		/// Writes out an array of bytes to the serial port.
		/// </summary>		
		/// <param name="bytes">
		/// The array of bytes to be written.
		/// </param>		
		/// <returns>
		/// A boolean value representing the success or failure of the operation.
		/// </returns>
		public bool Write(byte[] bytes)
		{			
			if (!this.isOpen)			
			{
				string errorMsg = "Cannot invode the Write method on the SerialPort " +
					"instance because the serial port is not open.";
				throw new ApplicationException(errorMsg);
			}

			bool isSuccessful = false;
			uint numberOfBytesWritten;

			if (bytes != null && bytes.GetLength(0) > 0)
			{
				isSuccessful = SerialCommAPI.WriteFile(this.handle, bytes, (uint)bytes.GetLength(0), 
					out numberOfBytesWritten, IntPtr.Zero);	
			
				if (isSuccessful && numberOfBytesWritten < (uint)bytes.GetLength(0))
					isSuccessful = false;				
			}
			else
			{
				string errorMsg = "No data to write to serial port.";
				throw new ApplicationException(errorMsg);
			}			

			return isSuccessful;
		}

		private bool InternalClear(bool clearRx, bool clearTx, bool clearErrors)
		{		
			if (this.handle == SerialCommAPI.INVALID_HANDLE_VALUE)
			{
				string errorMsg = "Cannot clear serial port errors and flush out " +
					"input and output buffers because the serial port is not open.";
				throw new ApplicationException(errorMsg);
			}
			
			uint errors;
			bool isSuccessful = false;
			
			// Clear serial port errors
			if (clearErrors)
			{
				isSuccessful = SerialCommAPI.ClearCommError(this.handle, out errors, IntPtr.Zero);
				if (!isSuccessful)			
					return false;							
			}

			// Flush I/O buffers
			uint purgeOption = (clearRx ? SerialCommAPI.PURGE_RXCLEAR : 0);
			purgeOption |= (clearTx ? SerialCommAPI.PURGE_TXCLEAR : 0);

			isSuccessful = SerialCommAPI.PurgeComm(this.handle, purgeOption);				
			
			return isSuccessful;				
		}

		/// <summary>
		/// Clears errors associated with the serial port
		/// and flushes its input and output buffers.
		/// </summary>
		/// <returns>
		/// A boolean value representing the success or failure of the operation.
		/// </returns>
		public bool Clear()
		{
			if (this.isOpen)
			{
				bool result = InternalClear(true, true, true);
				return result;
			}
			else
			{
				string errorMsg = "Cannot invode the Clear method on the SerialPort " +
					"instance because the serial port is not open.";
				throw new ApplicationException(errorMsg);
			}
		}

		/// <summary>
		/// Clears errors associated with the serial port
		/// and flushes its input and output buffers.
		/// </summary>
		/// <param name="clearErrors"></param>
		/// <param name="clearRx"></param>
		/// <param name="clearTx"></param>
		/// <returns>
		/// A boolean value representing the success or failure of the operation.
		/// </returns>
		public bool Clear(bool clearRx, bool clearTx, bool clearErrors)
		{
			if (this.isOpen)
			{
				bool result = InternalClear(clearErrors, clearRx, clearTx);
				return result;
			}
			else
			{
				string errorMsg = "Cannot invode the Clear method on the SerialPort " +
					"instance because the serial port is not open.";
				throw new ApplicationException(errorMsg);
			}
		}

		
		#endregion Methods		
	}	


	/// <summary>
	/// Encapsulates the Win32 serial commnunication API.	
	/// </summary>
	internal class SerialCommAPI
	{
		#region Constructor (private)

		private SerialCommAPI()
		{
		}

		#endregion Constructor

		#region Structs
	
		[StructLayout(LayoutKind.Sequential)] 
			public struct DCB 
		{
			public int DCBlength;
			public int BaudRate;
			public int Flags;
			public short wReserved;
			public short XonLim;
			public short XoffLim;
			public byte	ByteSize;
			public byte Parity;
			public byte StopBits;
			public byte XonChar;
			public byte XoffChar;
			public byte ErrorChar;
			public byte EofChar;
			public byte EvtChar;
			public short wReserved1;
		}

		[StructLayout(LayoutKind.Sequential)]
			public struct COMMTIMEOUTS 
		{			
			public uint ReadIntervalTimeout;
			public uint ReadTotalTimeoutMultiplier;
			public uint ReadTotalTimeoutConstant;
			public uint WriteTotalTimeoutMultiplier;
			public uint WriteTotalTimeoutConstant;
		}

		#endregion Structs

		#region Constants

		// Constant for the CreateFile function's return value. 
		public static readonly IntPtr INVALID_HANDLE_VALUE = new IntPtr(-1);		

		// Constants for the CreateFile function's dwCreationDisposition parameter.
		public const uint OPEN_EXISTING = 3;

		//Constants for the CreateFile function's dwDesiredAccess parameter.
		public const uint GENERIC_READ = 0x80000000;
		public const uint GENERIC_WRITE = 0x40000000;

		// Constants for PurgeComm function's dwFlags parameter.
		public const uint PURGE_TXABORT = 1;
		public const uint PURGE_RXABORT = 2;
		public const uint PURGE_TXCLEAR = 4;
		public const uint PURGE_RXCLEAR = 8;

		#endregion Constants 

		#region Functions

		[DllImport("kernel32.dll")]
		public static extern IntPtr CreateFile(string lpFileName, uint dwDesiredAccess, 
			uint dwShareMode, IntPtr lpSecurityAttributes, uint dwCreationDisposition, 
			uint dwFlagsAndAttributes, IntPtr hTemplateFile);	

		[DllImport("kernel32.dll")]
		public static extern bool CloseHandle(IntPtr hObject);		

		[DllImport("kernel32.dll")]
		public static extern bool ReadFile(IntPtr hFile, byte[] lpBuffer, 
			uint nNumberOfBytesToRead, out uint lpNumberOfBytesRead, IntPtr lpOverlapped);	

		[DllImport("kernel32.dll")]
		public static extern bool WriteFile(IntPtr hFile, byte[] lpBuffer, uint nNumberOfBytesToWrite,
			out uint lpNumberOfBytesWritten, IntPtr lpOverlapped);	

		[DllImport("kernel32.dll")]
		public static extern bool BuildCommDCB(string lpDef, ref DCB lpDCB);

		[DllImport("kernel32.dll")]
		public static extern bool GetCommState(IntPtr hFile, out DCB lpDCB);

		[DllImport("kernel32.dll")]
		public static extern bool GetCommTimeouts(IntPtr hFile, out COMMTIMEOUTS lpCommTimeouts);

		[DllImport("kernel32.dll")]
		public static extern bool SetCommState(IntPtr hFile, ref DCB lpDCB);

		[DllImport("kernel32.dll")]
		public static extern bool SetCommTimeouts(IntPtr hFile, ref COMMTIMEOUTS lpCommTimeouts);

		[DllImport("kernel32.dll")]
		public static extern bool SetupComm(IntPtr hFile, uint dwInQueue, uint dwOutQueue);

		[DllImport("kernel32.dll")]
		public static extern bool ClearCommError(IntPtr hFile, out uint lpErrors, IntPtr lpStat);

		[DllImport("kernel32.dll")]
		public static extern bool PurgeComm(IntPtr hFile, uint dwFlags);		

		#endregion Functions		
	}

	

	
}
